Skip to content

Add Gymnasium's 11 MuJoCo environments, played from pixels, with a Modal baseline job - #31

Open
r33drichards wants to merge 5 commits into
mainfrom
claude/exciting-carson-ujerli
Open

r33drichards wants to merge 5 commits into
mainfrom
claude/exciting-carson-ujerli

Conversation

@r33drichards

@r33drichards r33drichards commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

What

All eleven of Gymnasium's MuJoCo -v5 environments, played from the rendered screen with one choice per step. This works like laya/controlgames.py does for classic control.

  • laya/mujocogames.py: MujocoGame subclasses ControlGame, so it gets the same previous-frame ghosting, seeding and scoring. MuJoCo actions are continuous, so each game names a set of pushes:
    • InvertedPendulum / InvertedDoublePendulum push the cart (3 and 5 pushes), seen from a fixed side camera where a push right moves the cart right on screen.
    • Reacher, Pusher, Swimmer, Hopper, Walker2d, HalfCheetah, Ant, Humanoid, HumanoidStandup torque one joint at a time: NONE, then <JOINT>_POS / <JOINT>_NEG for each actuated joint. That is 5 options for Reacher up to 35 for Humanoid. Reacher and Pusher get closer cameras; the rest keep Gymnasium's tracking camera.
  • Experts, using the same named pushes as the model:
    • Pendulums: a linear controller, and LQR from a finite-difference linearization rounded to the nearest push. Both hold the pole for all 1000 steps.
    • Seven torque games: a lookahead planner. It holds each push for a few steps in the simulator, restores the state, and picks the best return. A test checks that it leaves the episode unchanged.
    • Pusher and Humanoid: no planner I tried beat doing nothing, so they have no expert and no normalized score.
  • Baselines: random_policy, and a new still_policy (always NONE), on the same seeds.
  • laya/controlgames.py: small hooks (SPECS, _make, _env_action, play_episodes(make=)), and solved_rate is None for games without a threshold. Classic-control behaviour is unchanged.
  • laya/games.py: goals and actions for the new games, generated from TORQUE_JOINTS. Existing questions don't change, so PROMPT_FORMAT_VERSION stays at "1".
  • Recording and baseline: laya.mujocogames.record writes a real-time .webm with the model's probabilities beside the screen, and baseline scores a checkpoint on one game. examples/mujoco_video.py and examples/mujoco_baseline.py run them locally.
  • modal_app.py::mujoco_eval: runs every game at once, one L4/A10G each (play_mujoco, rendering with OSMesa), and writes the videos and baseline.json locally. _load_policy_agent now accepts a Hub revision.
  • Docs: a "Play the MuJoCo environments" section in site-docs/how-to/play-games.md.

This PR doesn't add the MuJoCo games to the games suite and publishes no numbers.

Known issue: Humanoid prompts don't fit the question budget

At the released checkpoint's head_max_len=256, the 35 options of Humanoid and HumanoidStandup are cut to 5 tokens each, which leaves only 7 distinct option texts: ABDOMEN_Z_POS: torque the abdomen z one way (positive) becomes - ABDOMEN. Shorter wording can't fix it, because even RIGHT_SHOULDER_1_POS alone is longer than 5 tokens. The fix is a smaller action set for these two games (for example grouped limb moves) or a larger budget. That decision is still open, so the Humanoid rows below are not valid.

The other nine games keep every option distinct. Pusher (15 options), Walker2d and HalfCheetah (13 each) and Ant (17) lose part of each description, but no action names.

Zero-shot baseline (not a published number)

modal run modal_app.py::mujoco_eval --revision 8b318c99d7ad3ce19c24369263463882eada9d1e --max-steps 0: thaitea/laya-vision@8b318c9, 3 model episodes and 10 of each baseline from seed 200000, full-length episodes, about 26 minutes on Modal.

game model random still (NONE) expert normalized
InvertedPendulum 2.7 3.9 24.1 1000.0 -0.00
InvertedDoublePendulum 66.2 36.0 83.8 9350.3 0.00
Reacher -15.3 -15.7 -13.9 -10.8 0.08
Pusher -68.1 -65.7 -53.4 – –
Swimmer -20.8 1.9 3.7 37.5 -0.64
Hopper 95.5 13.7 169.7 340.4 0.25
Walker2d 36.7 6.4 94.1 331.1 0.09
HalfCheetah -95.1 -148.5 -0.5 127.5 0.19
Ant 852.7 387.3 992.4 1011.1 0.75
Humanoid ⚠ 155.8 142.1 198.9 – –
HumanoidStandup ⚠ 31188.8 29272.2 29837.4 54146.3 0.08

⚠ These rows are invalid: see the known issue above.

On the nine valid games, the model beats random on 7 and never beats doing nothing. Normalizing against random flatters it: on the robots that only need to stay upright, NONE is the stronger baseline. Ant's 0.75 is mostly survival, since the planner barely beats doing nothing there.

Testing

  • tests/test_mujocogames.py (skipped unless mujoco and gymnasium are installed): for every game, pushes match the action names and action space, and a frame renders. Experts beat random and still. The lookahead leaves the episode unchanged. Pendulum experts hold the pole to the limit. Also covers seeding, ghosting, and the questions. 34 passed together with tests/test_controlgames.py.
  • The full python -m pytest -q run on the first commit gave 304 passed and 6 tests/test_smolvlm2.py ImportErrors, which also fail on main in this sandbox.
  • mujoco_eval ran end to end on Modal: a 2-game probe, then all 11 games.
  • mkdocs build --strict passes (pip MkDocs; nix isn't available in the sandbox).

🤖 Generated with Claude Code

https://claude.ai/code/session_01X7wWfNMM9Rbk1757w3FaTH

…ixels

laya/mujocogames.py wraps Gymnasium's MuJoCo pendulums as laya.controlgames
wraps classic control: ghosted screen from a fixed side view, one choice over
named pushes (3 for the single pole, 5 for the double), seeded episodes, and
expert (linear controller / LQR rounded to a push) and random baselines.
ControlGame gains _make/_env_action hooks and play_episodes a make= argument
so the subclass reuses rendering and scoring. The new games' goals and actions
join CONTROL_GOALS / CONTROL_ACTIONS; existing questions are unchanged, so
PROMPT_FORMAT_VERSION stays.

examples/mujoco_video.py records an episode (expert, random or a checkpoint)
to .webm/.mp4 with the chosen push and the model's probabilities.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7wWfNMM9Rbk1757w3FaTH
…line script

Reacher, Pusher, Swimmer, Hopper, Walker2d, HalfCheetah, Ant, Humanoid and
HumanoidStandup join the pendulums in laya.mujocogames. They torque one joint
at a time: NONE plus <JOINT>_POS / <JOINT>_NEG per actuated joint
(laya.games.TORQUE_JOINTS), 5 to 35 options. The expert for seven of them is a
lookahead planner that holds each push for a few steps in the simulator and
restores it; on Pusher and Humanoid none beat doing nothing, so they have no
expert. still_policy (always NONE) joins random as a baseline, and
play_episodes reports solved_rate None for games without a threshold.

examples/mujoco_baseline.py records a checkpoint's first episode per game to
.webm and scores it against random, still and the expert on the same seeds;
mujoco_video.py gains record() and a panel that fits 35 actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7wWfNMM9Rbk1757w3FaTH
play_mujoco runs laya.mujocogames.baseline for one game on an L4/A10G (OSMesa
rendering, so nothing depends on the GPU driver's GL) and returns the model's
first-episode .webm with the scores; mujoco_eval spawns all eleven and writes
the videos and baseline.json locally. _load_policy_agent takes a Hub revision.

draw / record / model_chooser / baseline / baseline_table move from the
examples into laya.mujocogames so the Modal image (which ships laya/) can use
them; the examples call them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7wWfNMM9Rbk1757w3FaTH
@r33drichards r33drichards changed the title Add MuJoCo InvertedPendulum and InvertedDoublePendulum, played from pixels Add Gymnasium's 11 MuJoCo environments, played from pixels, with a Modal baseline job Sep 25, 2026
The nine torque envs now set every joint each step: one `choice` per joint
(laya.games.mujoco_questions) over STRONG_NEG / NEG / NONE / POS / STRONG_POS,
all answered in one predict (the frame is encoded once). Mapping a strong
expert's actions onto candidate spaces, one joint pushed per step kept at most
6% of its return on the walking robots and 8-128 presets at most 53%; five
levels per joint kept 78-100%. It also fixes Humanoid's 35 options collapsing
to 7 under the 256-token question budget (a test now checks every question
keeps its options distinct).

The experts are the Farama Foundation's SB3 policies on the Hub
(mujocogames.HUB_EXPERTS, pinned by commit), rounded per joint; Pusher and
Humanoid now have one. The one-joint lookahead planner is gone. Pendulums keep
their single choice and scripted experts. ControlGame._env_action takes the
action itself, play_episodes counts per-joint levels, and the video panel shows
each joint's five levels. The Modal MuJoCo image gains stable-baselines3 and
sb3-contrib.

New questions only; existing games' input ids are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7wWfNMM9Rbk1757w3FaTH
laya.mujocogames.expert_frames yields frames of the Hub expert's play,
rendered exactly as in play, with one record per question asked about the
frame: per joint, a soft target splitting its weight between the two levels
nearest the expert's continuous action (soft_levels), label = argmax. The
behaviour policy jitters each joint's level one notch with probability 0.1
(DART-style); uniformly random actions toppled the walkers and pendulums within
100-250 steps even at 5%.

modal_app.py::prepare_mujoco writes /data/vqa/mujoco_<game> (refuses an
existing name) with meta, manifest (expert repo and commit) and _READY;
prepare_mujoco_all fans out one CPU container per game.

MujocoGame._make imports torch._dynamo before gymnasium/mujoco: with
MUJOCO_GL=osmesa, OSMesa's LLVM loaded first and triton's bundled LLVM
(pulled in when an SB3 expert loads) then segfaulted the process. The Modal
MuJoCo image sets PYTHONFAULTHANDLER=1 so a native crash prints a stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7wWfNMM9Rbk1757w3FaTH

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants